# +----------------------------------------------------------------------
# | Pink [ A modern python web framework ]
# +----------------------------------------------------------------------
# | Copyright (c) 2023 http:#unnnnn.com All rights reserved.
# +----------------------------------------------------------------------
# | Author: chenjianhua <unnnnn@foxmail.com>
# +----------------------------------------------------------------------

import inspect
import mimetypes
import os
import re

from .MessageBag import MessageBag
from .RuleEnclosure import RuleEnclosure
from ..configuration import config
from ..facades import Loader
from ..filesystem import UploadedFile
from ..utils.structures import data_get


class BaseValidation:
    def __init__(self, validations, messages={}, raises={}):
        self.errors = {}
        self.messages = messages
        if isinstance(validations, str):
            self.validations = [validations]
        else:
            self.validations = validations
        self.negated = False
        self.raises = raises

    def passes(self, attribute, key, dictionary):
        return True

    def error(self, key, message):
        if key in self.messages:
            if key in self.errors:
                self.errors[key].append(self.messages[key])
                return
            self.errors.update({key: [self.messages[key]]})
            return

        if not isinstance(message, list):
            self.errors.update({key: [message]})
        else:
            self.errors.update({key: message})

    def find(self, key, dictionary, default=""):
        return data_get(dictionary, key, default)

    def message(self, key):
        return ""

    def negate(self):
        self.negated = True
        return self

    def raise_exception(self, key):
        if self.raises is not True and key in self.raises:
            error = self.raises.get(key)
            raise error(self.errors[next(iter(self.errors))][0])

        raise ValueError(self.errors[next(iter(self.errors))][0])

    def handle(self, dictionary):
        boolean = True

        for key in self.validations:
            if self.negated:
                if self.passes(self.find(key, dictionary), key, dictionary):
                    boolean = False
                    if hasattr(self, "negated_message"):
                        self.error(key, self.negated_message(key))
                    else:
                        self.error(key, self.message(key))

                continue
            attribute = self.find(key, dictionary)
            if not self.passes(attribute, key, dictionary):
                boolean = False
                self.error(key, self.message(key))

            if self.errors and self.raises:
                return self.raise_exception(key)

        return boolean

    def reset(self):
        self.errors = {}


class required(BaseValidation):
    def passes(self, attribute, key, dictionary):
        """The passing criteria for this rule.

        The key must exist in the dictionary and return a True boolean value.
        The key can use * notation.

        Arguments:
            attribute {mixed} -- The value found within the dictionary
            key {string} -- The key in the dictionary being searched for.
            dictionary {dict} -- The dictionary being searched

        Returns:
            bool
        """
        return self.find(key, dictionary) and attribute

    def message(self, key):
        """A message to show when this rule fails

        Arguments:
            key {string} -- The key used to search the dictionary

        Returns:
            string
        """
        return "The {} field is required.".format(key)

    def negated_message(self, key):
        """A message to show when this rule is negated using a negation rule like 'isnt()'

        For example if you have a message that says 'this is required' you may have a negated statement
        that says 'this is not required'.

        Arguments:
            key {string} -- The key used to search the dictionary

        Returns:
            string
        """
        return "The {} field is not required.".format(key)


class timezone(BaseValidation):
    def passes(self, attribute, key, dictionary):
        import pytz

        return attribute in pytz.all_timezones

    def message(self, attribute):
        return "The {} must be a valid timezone.".format(attribute)

    def negated_message(self, attribute):
        return "The {} must not be a valid timezone.".format(attribute)


class one_of(BaseValidation):
    def passes(self, attribute, key, dictionary):
        for validation in self.validations:
            if validation in dictionary:
                return True

        return False

    def message(self, attribute):
        if len(self.validations) > 2:
            text = ", ".join(self.validations)
        else:
            text = " or ".join(self.validations)

        return "The {} is required.".format(text)

    def negated_message(self, attribute):
        if len(self.validations) > 2:
            text = ", ".join(self.validations)
        else:
            text = " or ".join(self.validations)

        return "The {} is not required.".format(text)


class boolean(BaseValidation):
    def passes(self, attribute, key, dictionary):
        return attribute in [True, False, 0, 1, "0", "1"]

    def message(self, attribute):
        return "The {} must be a boolean.".format(attribute)

    def negated_message(self, attribute):
        return "The {} must not be a boolean.".format(attribute)


class accepted(BaseValidation):
    def passes(self, attribute, key, dictionary):
        return (
            attribute is True
            or attribute == "on"
            or attribute == "yes"
            or attribute == "1"
            or attribute == 1
        )

    def message(self, attribute):
        return "The {} must be accepted.".format(attribute)

    def negated_message(self, attribute):
        return "The {} must not be accepted.".format(attribute)


class ip(BaseValidation):
    def passes(self, attribute, key, dictionary):
        import socket

        try:
            socket.inet_aton(attribute)
            return True
        except socket.error:
            return False

    def message(self, attribute):
        return "The {} must be a valid ipv4 address.".format(attribute)

    def negated_message(self, attribute):
        return "The {} must not be a valid ipv4 address.".format(attribute)


class date(BaseValidation):
    def passes(self, attribute, key, dictionary):
        import pendulum

        try:
            date = pendulum.parse(attribute)
            return date
        except pendulum.parsing.exceptions.ParserError:
            return False

    def message(self, attribute):
        return "The {} must be a valid date.".format(attribute)

    def negated_message(self, attribute):
        return "The {} must not be a valid date.".format(attribute)


class before_today(BaseValidation):
    def __init__(self, validations, tz="UTC", messages={}, raises={}):
        super().__init__(validations, messages=messages, raises=raises)
        self.tz = tz

    def passes(self, attribute, key, dictionary):
        import pendulum

        try:
            return pendulum.parse(attribute, tz=self.tz) <= pendulum.yesterday()
        except pendulum.parsing.exceptions.ParserError:
            return False

    def message(self, attribute):
        return "The {} must be a date before today.".format(attribute)

    def negated_message(self, attribute):
        return "The {} must not be a date before today.".format(attribute)


class after_today(BaseValidation):
    def __init__(self, validations, tz="Universal", messages={}, raises={}):
        super().__init__(validations, messages=messages, raises=raises)
        self.tz = tz

    def passes(self, attribute, key, dictionary):
        import pendulum

        try:
            return pendulum.parse(attribute, tz=self.tz) >= pendulum.yesterday()
        except pendulum.parsing.exceptions.ParserError:
            return False

    def message(self, attribute):
        return "The {} must be a date after today.".format(attribute)

    def negated_message(self, attribute):
        return "The {} must not be a date after today.".format(attribute)


class is_past(BaseValidation):
    def __init__(self, validations, tz="Universal", messages={}, raises={}):
        super().__init__(validations, messages=messages, raises=raises)
        self.tz = tz

    def passes(self, attribute, key, dictionary):
        import pendulum

        try:
            return pendulum.parse(attribute, tz=self.tz).is_past()
        except pendulum.parsing.exceptions.ParserError:
            return False

    def message(self, attribute):
        return "The {} must be a time in the past.".format(attribute)

    def negated_message(self, attribute):
        return "The {} must not be a time in the past.".format(attribute)


class is_future(BaseValidation):
    def __init__(self, validations, tz="Universal", messages={}, raises={}):
        super().__init__(validations, messages=messages, raises=raises)
        self.tz = tz

    def passes(self, attribute, key, dictionary):
        import pendulum

        try:
            return pendulum.parse(attribute, tz=self.tz).is_future()
        except pendulum.parsing.exceptions.ParserError:
            return False

    def message(self, attribute):
        return "The {} must be a time in the past.".format(attribute)

    def negated_message(self, attribute):
        return "The {} must not be a time in the past.".format(attribute)


class email(BaseValidation):
    def passes(self, attribute, key, dictionary):
        return re.compile(
            r"^[^.][^@]*@([?)[a-zA-Z0-9-.])+.([a-zA-Z]{2,3}|[0-9]{1,3})(]?)$"
        ).match(attribute)

    def message(self, attribute):
        return "The {} must be a valid email address.".format(attribute)

    def negated_message(self, attribute):
        return "The {} must not be a valid email address.".format(attribute)


class matches(BaseValidation):
    def __init__(self, validations, match, messages={}, raises={}):
        super().__init__(validations, messages=messages, raises=raises)
        self.match = match

    def passes(self, attribute, key, dictionary):
        return attribute == dictionary[self.match]

    def message(self, attribute):
        return "The {} must match {}.".format(attribute, self.match)

    def negated_message(self, attribute):
        return "The {} must not match {}.".format(attribute, self.match)


class exists(BaseValidation):
    def passes(self, attribute, key, dictionary):
        return key in dictionary

    def message(self, attribute):
        return "The {} must exist.".format(attribute)

    def negated_message(self, attribute):
        return "The {} must not exist.".format(attribute)


def resolve_model_or_table(string):
    # it means that a python model path has been given
    if "." in string:
        model_name = string.split(".")[-1]
        model_class = Loader.get_object(string, model_name)
        table = model_class().get_table_name()
        return table, model_class
    else:
        return string, None


class exists_in_db(BaseValidation):
    """A record with field equal to the given value should exists in the table/model specified."""

    def __init__(
        self,
        validations,
        table_or_model,
        column=None,
        connection="default",
        messages={},
        raises={},
    ):
        super().__init__(validations, messages=messages, raises=raises)
        self.connection = config("database.db").get_query_builder(connection)
        self.column = column
        self.table, self.model = resolve_model_or_table(table_or_model)

    def passes(self, attribute, key, dictionary):
        column = key if not self.column else self.column
        count = self.connection.table(self.table).where(column, attribute).count()
        return count

    def message(self, attribute):
        return "No record found in table {} with the same {}.".format(
            self.table, attribute
        )

    def negated_message(self, attribute):
        return "A record already exists in table {} with the same {}.".format(
            self.table, attribute
        )


class not_exists_in_db(BaseValidation):
    """A record with field equal to the given value should exists in the table/model specified."""

    def __init__(
        self,
        validations,
        table_or_model,
        column=None,
        connection="default",
        messages={},
        raises={},
    ):
        super().__init__(validations, messages=messages, raises=raises)
        self.connection = config("database.db").get_query_builder(connection)
        self.column = column
        self.table, self.model = resolve_model_or_table(table_or_model)

    def passes(self, attribute, key, dictionary):
        column = key if not self.column else self.column
        count = self.connection.table(self.table).where(column, attribute).count()
        return count == 0

    def message(self, attribute):
        return "A record already exists in table {} with the same {}".format(
            self.table, attribute
        )

    def negated_message(self, attribute):
        return "No record found in table {} with the same {}".format(
            self.table, attribute
        )


class unique_in_db(BaseValidation):
    """No record should exist for the field under validation within the given table/model."""

    def __init__(
        self,
        validations,
        table_or_model,
        column=None,
        connection="default",
        messages={},
        raises={},
    ):
        super().__init__(validations, messages=messages, raises=raises)
        self.connection = config("database.db").get_query_builder(connection)
        self.column = column
        self.table, self.model = resolve_model_or_table(table_or_model)

    def passes(self, attribute, key, dictionary):
        column = key if not self.column else self.column
        count = self.connection.table(self.table).where(column, attribute).count()
        return count == 0

    def message(self, attribute):
        return "A record already exists in table {} with the same {}.".format(
            self.table, attribute
        )

    def negated_message(self, attribute):
        return "A record should exist in table {} with the same {}.".format(
            self.table, attribute
        )


class active_domain(BaseValidation):
    def passes(self, attribute, key, dictionary):
        import socket

        try:
            if "@" in attribute:
                # validation is for an email address
                return socket.gethostbyname(attribute.split("@")[1])

            return socket.gethostbyname(
                attribute.replace("https://", "")
                .replace("http://", "")
                .replace("www.", "")
            )
        except socket.gaierror:
            return False

    def message(self, attribute):
        return "The {} must be an active domain name.".format(attribute)

    def negated_message(self, attribute):
        return "The {} must not be an active domain name.".format(attribute)


class numeric(BaseValidation):
    def passes(self, attribute, key, dictionary):
        if isinstance(attribute, list):
            for value in attribute:
                if not str(value).replace(".", "", 1).isdigit():
                    return False
        else:
            return str(attribute).replace(".", "", 1).isdigit()

        return True

    def message(self, attribute):
        return "The {} must be a numeric.".format(attribute)

    def negated_message(self, attribute):
        return "The {} must not be a numeric.".format(attribute)


class is_list(BaseValidation):
    def passes(self, attribute, key, dictionary):
        return isinstance(attribute, list)

    def message(self, attribute):
        return "The {} must be a list.".format(attribute)

    def negated_message(self, attribute):
        return "The {} must not be a list.".format(attribute)


class string(BaseValidation):
    def passes(self, attribute, key, dictionary):
        if isinstance(attribute, list):
            for attr in attribute:
                if not isinstance(attr, str):
                    return False

            return True

        return isinstance(attribute, str)

    def message(self, attribute):
        return "The {} must be a string.".format(attribute)

    def negated_message(self, attribute):
        return "The {} must not be a string.".format(attribute)


class none(BaseValidation):
    def passes(self, attribute, key, dictionary):
        return attribute is None

    def message(self, attribute):
        return "The {} must be None.".format(attribute)

    def negated_message(self, attribute):
        return "The {} must not be None.".format(attribute)


class length(BaseValidation):
    def __init__(self, validations, min=0, max=False, messages={}, raises={}):
        super().__init__(validations, messages=messages, raises=raises)
        if isinstance(min, str) and ".." in min:
            self.min = int(min.split("..")[0])
            self.max = int(min.split("..")[1])
        else:
            self.min = min
            self.max = max

    def passes(self, attribute, key, dictionary):
        if not hasattr(attribute, "__len__"):
            attribute = str(attribute)
        if self.max:
            return len(attribute) >= self.min and len(attribute) <= self.max
        else:
            return len(attribute) >= self.min

    def message(self, attribute):
        if self.min and not self.max:
            return "The {} must be at least {} characters.".format(attribute, self.min)
        else:
            return "The {} length must be between {} and {}.".format(
                attribute, self.min, self.max
            )

    def negated_message(self, attribute):
        if self.min and not self.max:
            return "The {} must be {} characters maximum.".format(attribute, self.max)
        else:
            return "The {} length must not be between {} and {}.".format(
                attribute, self.min, self.max
            )


class in_range(BaseValidation):
    def __init__(self, validations, min=1, max=255, messages={}, raises={}):
        super().__init__(validations, messages=messages, raises=raises)
        self.min = min
        self.max = max

    def passes(self, attribute, key, dictionary):
        attribute = str(attribute)

        if attribute.isalpha():
            return False

        if "." in attribute:
            try:
                attribute = float(attribute)
            except Exception:
                pass

        elif attribute.isdigit():
            attribute = int(attribute)

        return attribute >= self.min and attribute <= self.max

    def message(self, attribute):
        return "The {} must be between {} and {}.".format(attribute, self.min, self.max)

    def negated_message(self, attribute):
        return "The {} must not be between {} and {}.".format(
            attribute, self.min, self.max
        )


class equals(BaseValidation):
    def __init__(self, validations, value="", messages={}, raises={}):
        super().__init__(validations, messages=messages, raises=raises)
        self.value = value

    def passes(self, attribute, key, dictionary):
        return attribute == self.value

    def message(self, attribute):
        return "The {} must be equal to {}.".format(attribute, self.value)

    def negated_message(self, attribute):
        return "The {} must not be equal to {}.".format(attribute, self.value)


class contains(BaseValidation):
    def __init__(self, validations, value="", messages={}, raises={}):
        super().__init__(validations, messages=messages, raises=raises)
        self.value = value

    def passes(self, attribute, key, dictionary):
        return self.value in attribute

    def message(self, attribute):
        return "The {} must contain {}.".format(attribute, self.value)

    def negated_message(self, attribute):
        return "The {} must not contain {}.".format(attribute, self.value)


class is_in(BaseValidation):
    def __init__(self, validations, value="", messages={}, raises={}):
        super().__init__(validations, messages=messages, raises=raises)
        self.value = value

    def passes(self, attribute, key, dictionary):
        return attribute in self.value

    def message(self, attribute):
        return "The {} must contain an element in {}.".format(attribute, self.value)

    def negated_message(self, attribute):
        return "The {} must not contain an element in {}.".format(attribute, self.value)


class greater_than(BaseValidation):
    def __init__(self, validations, value="", messages={}, raises={}):
        super().__init__(validations, messages=messages, raises=raises)
        self.value = value

    def passes(self, attribute, key, dictionary):
        return int(attribute) > int(self.value)

    def message(self, attribute):
        return "The {} must be greater than {}.".format(attribute, self.value)

    def negated_message(self, attribute):
        return "The {} must be greater than {}.".format(attribute, self.value)


class less_than(BaseValidation):
    def __init__(self, validations, value="", messages={}, raises={}):
        super().__init__(validations, messages=messages, raises=raises)
        self.value = value

    def passes(self, attribute, key, dictionary):
        return attribute < self.value

    def message(self, attribute):
        return "The {} must be less than {}.".format(attribute, self.value)

    def negated_message(self, attribute):
        return "The {} must not be less than {}.".format(attribute, self.value)


class strong(BaseValidation):
    def __init__(
        self,
        validations,
        length=8,
        uppercase=2,
        lowercase=2,
        numbers=2,
        special=2,
        special_chars=None,
        breach=False,
        messages={},
        raises={},
    ):
        super().__init__(validations, messages=messages, raises=raises)
        self.length = length
        self.uppercase = uppercase
        self.lowercase = lowercase
        self.numbers = numbers
        self.special = special
        self.special_chars = special_chars
        self.breach = breach
        self.length_check = True
        self.uppercase_check = True
        self.lowercase_check = True
        self.numbers_check = True
        self.special_check = True
        self.breach_check = True

    def passes(self, attribute, key, dictionary):
        all_clear = True

        if len(attribute) < self.length:
            all_clear = False
            self.length_check = False

        if self.uppercase != 0:
            uppercase = 0
            for letter in attribute:
                if letter.isupper():
                    uppercase += 1

            if uppercase < self.uppercase:
                self.uppercase_check = False
                all_clear = False

        if self.lowercase != 0:
            lowercase = 0
            for letter in attribute:
                if letter.islower():
                    lowercase += 1

            if lowercase < self.lowercase:
                self.lowercase_check = False
                all_clear = False

        if self.numbers != 0:
            numbers = 0
            for letter in attribute:
                if letter.isdigit():
                    numbers += 1

            if numbers < self.numbers:
                self.numbers_check = False
                all_clear = False

        if self.breach:
            try:
                from pwnedapi import Password
            except ImportError:
                raise ImportError(
                    "Checking for breaches requires the 'pwnedapi' library. Please install it with 'pip install pwnedapi'"
                )

            password = Password(attribute)
            if password.is_pwned():
                self.breach_check = False
                all_clear = False

        if self.special != 0:
            special_chars = "[^A-Za-z0-9]"
            if self.special_chars:
                special_chars = f"[{self.special_chars}]"
            if len(re.findall(special_chars, attribute)) < self.special:
                self.special_check = False
                all_clear = False

        return all_clear

    def message(self, attribute):
        message = []
        if not self.length_check:
            message.append(
                "The {} field must be {} characters in length".format(
                    attribute, self.length
                )
            )

        if not self.uppercase_check:
            message.append(
                "The {} field must have {} uppercase letters".format(
                    attribute, self.uppercase
                )
            )

        if not self.lowercase_check:
            message.append(
                "The {} field must have {} lowercase letters".format(
                    attribute, self.lowercase
                )
            )

        if not self.special_check:
            valid_chars = self.special_chars or "!@#$%^&*()_+"
            message.append(
                "The {} field must contain at least {} of these characters: '{}'".format(
                    attribute, self.special, valid_chars
                )
            )

        if not self.numbers_check:
            message.append(
                "The {} field must have {} numbers".format(attribute, self.numbers)
            )

        if not self.breach_check:
            message.append(
                "The {} field has been breached in the past. Try another {}".format(
                    attribute, attribute
                )
            )

        return message

    def negated_message(self, attribute):
        return "The {} must not be less than {}.".format(attribute, self.value)


class isnt(BaseValidation):
    def __init__(self, *rules, messages={}, raises={}):
        super().__init__(rules)

    def handle(self, dictionary):
        for rule in self.validations:
            rule.negate().handle(dictionary)
            self.errors.update(rule.errors)


class does_not(BaseValidation):
    def __init__(self, *rules, messages={}, raises={}):
        super().__init__(rules)
        self.should_run_then = True

    def handle(self, dictionary):
        self.dictionary = dictionary
        errors = False
        for rule in self.validations:
            if rule.handle(dictionary):
                errors = True

        if not errors:
            for rule in self.then_rules:
                if not rule.handle(dictionary):
                    self.errors.update(rule.errors)

    def then(self, *rules):
        self.then_rules = rules
        return self


class when(BaseValidation):
    def __init__(self, *rules, messages={}, raises={}):
        super().__init__(rules)
        self.should_run_then = True

    def handle(self, dictionary):
        self.dictionary = dictionary
        errors = False
        for rule in self.validations:
            if rule.handle(dictionary):
                errors = True

        if errors:
            for rule in self.then_rules:
                if not rule.handle(dictionary):
                    self.errors.update(rule.errors)

    def then(self, *rules):
        self.then_rules = rules
        return self


class truthy(BaseValidation):
    def passes(self, attribute, key, dictionary):
        return attribute

    def message(self, attribute):
        return "The {} must be a truthy value.".format(attribute)

    def negated_message(self, attribute):
        return "The {} must not be a truthy value.".format(attribute)


class json(BaseValidation):
    def passes(self, attribute, key, dictionary):
        import json as json_module

        try:
            return json_module.loads(str(attribute))
        except (TypeError, json_module.decoder.JSONDecodeError):
            return False

    def message(self, attribute):
        return "The {} must be a valid JSON.".format(attribute)

    def negated_message(self, attribute):
        return "The {} must not be a valid JSON.".format(attribute)


class phone(BaseValidation):
    def __init__(self, *rules, pattern="123-456-7890", messages={}, raises={}):
        super().__init__(rules, messages=messages, raises=raises)
        # 123-456-7890
        # (123)456-7890
        self.pattern = pattern

    def passes(self, attribute, key, dictionary):
        if self.pattern == "(123)456-7890":
            return re.compile(r"^\(\w{3}\)\w{3}\-\w{4}$").match(attribute)
        elif self.pattern == "123-456-7890":
            return re.compile(r"^\w{3}\-\w{3}\-\w{4}$").match(attribute)

    def message(self, attribute):
        if self.pattern == "(123)456-7890":
            return "The {} must be in the format (XXX)XXX-XXXX.".format(attribute)
        elif self.pattern == "123-456-7890":
            return "The {} must be in the format XXX-XXX-XXXX.".format(attribute)

    def negated_message(self, attribute):
        if self.pattern == "(123)456-7890":
            return "The {} must not be in the format (XXX)XXX-XXXX.".format(attribute)
        elif self.pattern == "123-456-7890":
            return "The {} must not be in the format XXX-XXX-XXXX.".format(attribute)


class confirmed(BaseValidation):
    def passes(self, attribute, key, dictionary):
        if key in dictionary and key + "_confirmation" in dictionary:
            return dictionary[key] == dictionary["{}".format(key + "_confirmation")]
        return False

    def message(self, attribute):
        return "The {} confirmation does not match.".format(attribute)

    def negated_message(self, attribute):
        return "The {} confirmation matches.".format(attribute)


class regex(BaseValidation):
    def __init__(self, validations, pattern, messages={}, raises={}):
        super().__init__(validations, messages=messages, raises=raises)
        self.pattern = pattern

    def passes(self, attribute, key, dictionary):
        return re.compile(r"{}".format(self.pattern)).match(attribute)

    def message(self, attribute):
        return "The {} does not match pattern {} .".format(attribute, self.pattern)

    def negated_message(self, attribute):
        return "The {} matches pattern {} .".format(attribute, self.pattern)


def parse_size(size):
    """Parse humanized size into bytes"""
    from hfilesize import FileSize

    return FileSize(size, case_sensitive=False)


class BaseFileValidation(BaseValidation):
    def __init__(self, validations, messages={}, raises={}):
        super().__init__(validations, messages=messages, raises=raises)
        self.file_check = True
        self.size_check = True
        self.mimes_check = True
        self.all_clear = True

    def passes(self, attribute, key, dictionary):
        # Here we can validate on a filepath or an uploaded file (from request)
        if isinstance(attribute, UploadedFile):
            filepath = attribute.name
            file_size = attribute.size
        else:
            filepath = attribute
            if not os.path.isfile(filepath):
                self.file_check = False
                return False
            file_size = os.path.getsize(filepath)
        # validate size
        if self.size:
            if file_size > self.size:
                self.size_check = False
                self.all_clear = False
        # validate extension
        if self.allowed_extensions:
            mimetype, encoding = mimetypes.guess_type(filepath)
            if mimetype not in self.allowed_mimetypes:
                self.mimes_check = False
                self.all_clear = False
        return self.all_clear


class file(BaseFileValidation):
    def __init__(self, validations, size=False, mimes=False, messages={}, raises={}):
        super().__init__(validations, messages=messages, raises=raises)
        self.size = parse_size(size)

        # parse allowed extensions to a list of mime types
        self.allowed_extensions = mimes
        if mimes:
            self.allowed_mimetypes = list(
                map(lambda mt: mimetypes.types_map.get("." + mt, None), mimes)
            )

    def message(self, attribute):
        messages = []
        if not self.file_check:
            messages.append("The {} is not a valid file.".format(attribute))

        if not self.size_check:
            from hfilesize import FileSize

            messages.append(
                "The {} file size exceeds {:.02fH}.".format(
                    attribute, FileSize(self.size)
                )
            )
        if not self.mimes_check:
            messages.append(
                "The {} mime type is not valid. Allowed formats are {}.".format(
                    attribute, ",".join(self.allowed_extensions)
                )
            )

        return messages

    def negated_message(self, attribute):
        messages = []
        if self.file_check:
            messages.append("The {} is a valid file.".format(attribute))
        if self.size_check:
            from hfilesize import FileSize

            messages.append(
                "The {} file size is less or equal than {:.02fH}.".format(
                    attribute, FileSize(self.size)
                )
            )
        if self.mimes_check:
            messages.append(
                "The {} mime type is in {}.".format(
                    attribute, ",".join(self.allowed_extensions)
                )
            )
        return messages


class image(BaseFileValidation):
    def __init__(self, validations, size=False, messages={}, raises={}):
        super().__init__(validations, messages=messages, raises=raises)
        self.size = parse_size(size)
        image_mimetypes = {
            ext: mimetype
            for ext, mimetype in mimetypes.types_map.items()
            if mimetype.startswith("image")
        }
        self.allowed_extensions = list(image_mimetypes.keys())
        self.allowed_mimetypes = list(image_mimetypes.values())

    def message(self, attribute):
        messages = []
        if not self.file_check:
            messages.append("The {} is not a valid file.".format(attribute))

        if not self.size_check:
            from hfilesize import FileSize

            messages.append(
                "The {} file size exceeds {:.02fH}.".format(
                    attribute, FileSize(self.size)
                )
            )

        if not self.mimes_check:
            messages.append(
                "The {} file is not a valid image. Allowed formats are {}.".format(
                    attribute, ",".join(self.allowed_extensions)
                )
            )

        return messages

    def negated_message(self, attribute):
        messages = []
        if self.file_check:
            messages.append("The {} is a valid file.".format(attribute))
        if self.size_check:
            from hfilesize import FileSize

            messages.append(
                "The {} file size is less or equal than {:.02fH}.".format(
                    attribute, FileSize(self.size)
                )
            )

        if self.mimes_check:
            messages.append("The {} file is a valid image.".format(attribute))

        return messages


class video(BaseFileValidation):
    def __init__(self, validations, size=False, messages={}, raises={}):
        super().__init__(validations, messages=messages, raises=raises)
        self.size = parse_size(size)

        video_mimetypes = {
            ext: mimetype
            for ext, mimetype in mimetypes.types_map.items()
            if mimetype.startswith("video")
        }

        self.allowed_extensions = list(video_mimetypes.keys())
        self.allowed_mimetypes = list(video_mimetypes.values())

    def message(self, attribute):
        messages = []
        if not self.file_check:
            messages.append("The {} is not a valid file.".format(attribute))

        if not self.size_check:
            from hfilesize import FileSize

            messages.append(
                "The {} file size exceeds {:.02fH}.".format(
                    attribute, FileSize(self.size)
                )
            )

        if not self.mimes_check:
            messages.append(
                "The {} file is not a valid video. Allowed formats are {}.".format(
                    attribute, ",".join(self.allowed_extensions)
                )
            )

        return messages

    def negated_message(self, attribute):
        messages = []
        if self.file_check:
            messages.append("The {} is a valid file.".format(attribute))

        if self.size_check:
            from hfilesize import FileSize

            messages.append(
                "The {} file size is less or equal than {:.02fH}.".format(
                    attribute, FileSize(self.size)
                )
            )

        if self.mimes_check:
            messages.append("The {} file is a valid video.".format(attribute))

        return messages


class postal_code(BaseValidation):
    def __init__(self, validations, locale, messages={}, raises={}):
        super().__init__(validations, messages=messages, raises=raises)
        from .resources.postal_codes import PATTERNS

        self.locales = []
        self.patterns = []
        self.patterns_example = []
        self.locales = locale.split(",")

        for locale in self.locales:
            pattern_dict = PATTERNS.get(locale, None)
            if pattern_dict is None or pattern_dict["pattern"] is None:
                raise NotImplementedError(
                    "Unsupported country code {}. Check that it is a ISO 3166-1 country code or open a PR to require support of this country code.".format(
                        locale
                    )
                )
            else:
                self.patterns.append(pattern_dict["pattern"])
                self.patterns_example.append(pattern_dict["example"])

    def passes(self, attribute, key, dictionary):
        for pattern in self.patterns:
            # check that at least one pattern match attribute
            if re.compile(r"{}".format(pattern)).match(attribute):
                return True
        return False

    def message(self, attribute):
        return "The {} is not a valid {} postal code. Valid {} {}.".format(
            attribute,
            ",".join(self.locales),
            "examples are" if len(self.locales) > 1 else "example is",
            ",".join(self.patterns_example),
        )

    def negated_message(self, attribute):
        return "The {} is a valid {} postal code.".format(attribute, self.locale)


class different(BaseValidation):
    """The field under validation must be different than an other given field."""

    def __init__(self, validations, other_field, messages={}, raises={}):
        super().__init__(validations, messages=messages, raises=raises)
        self.other_field = other_field

    def passes(self, attribute, key, dictionary):
        other_value = dictionary.get(self.other_field, None)
        return attribute != other_value

    def message(self, attribute):
        return "The {} value must be different than {} value.".format(
            attribute, self.other_field
        )

    def negated_message(self, attribute):
        return "The {} value be the same as {} value.".format(
            attribute, self.other_field
        )


class uuid(BaseValidation):
    """The field under validation must be a valid UUID. The UUID version standard
    can be precised (1,3,4,5)."""

    def __init__(self, validations, version=4, messages={}, raises={}):
        super().__init__(validations, messages=messages, raises=raises)
        self.version = version
        self.uuid_type = "UUID"
        if version:
            self.uuid_type = "UUID {0}".format(self.version)

    def passes(self, attribute, key, dictionary):
        from uuid import UUID

        try:
            uuid_value = UUID(str(attribute))
            return uuid_value.version == int(self.version)
        except ValueError:
            return False

    def message(self, attribute):
        return "The {} value must be a valid {}.".format(attribute, self.uuid_type)

    def negated_message(self, attribute):
        return "The {} value must not be a valid {}.".format(attribute, self.uuid_type)


class required_if(BaseValidation):
    """The field under validation must be present and not empty only
    if an other field has a given value."""

    def __init__(self, validations, other_field, value, messages={}, raises={}):
        super().__init__(validations, messages=messages, raises=raises)
        self.other_field = other_field
        self.value = value

    def passes(self, attribute, key, dictionary):
        if dictionary.get(self.other_field, None) == self.value:
            return required.passes(self, attribute, key, dictionary)

        return True

    def message(self, attribute):
        return "The {} is required because {}={}.".format(
            attribute, self.other_field, self.value
        )

    def negated_message(self, attribute):
        return "The {} is not required because {}={} or {} is not present.".format(
            attribute, self.other_field, self.value, self.other_field
        )


class required_with(BaseValidation):
    """The field under validation must be present and not empty only
    if any of the other specified fields are present."""

    def __init__(self, validations, other_fields, messages={}, raises={}):
        super().__init__(validations, messages=messages, raises=raises)
        if not isinstance(other_fields, list):
            if "," in other_fields:
                self.other_fields = other_fields.split(",")
            else:
                self.other_fields = [other_fields]
        else:
            self.other_fields = other_fields

    def passes(self, attribute, key, dictionary):
        for field in self.other_fields:
            if field in dictionary:
                return required.passes(self, attribute, key, dictionary)
        else:
            return True

    def message(self, attribute):
        fields = ",".join(self.other_fields)
        return "The {} is required because {} is present.".format(
            attribute,
            "one in {}".format(fields)
            if len(self.other_fields) > 1
            else self.other_fields[0],
        )

    def negated_message(self, attribute):
        return "The {} is not required because {} {} is not present.".format(
            attribute,
            "none of" if len(self.other_fields) > 1 else "",
            ",".join(self.other_fields),
        )


class distinct(BaseValidation):
    """When working with list, the field under validation must not have any
    duplicate values."""

    def passes(self, attribute, key, dictionary):
        # check if list contains duplicates
        return len(set(attribute)) == len(attribute)

    def message(self, attribute):
        return "The {} field has duplicate values.".format(attribute)

    def negated_message(self, attribute):
        return "The {} field has only different values.".format(attribute)


class Validator:
    def __init__(self):
        pass

    def validate(self, dictionary, *rules):
        rule_errors = {}
        try:
            for rule in rules:
                if isinstance(rule, str):
                    rule = self.parse_string(rule)
                    # continue
                elif isinstance(rule, dict):
                    rule = self.parse_dict(rule, dictionary, rule_errors)
                    continue

                elif inspect.isclass(rule) and isinstance(rule(), RuleEnclosure):
                    rule_errors.update(self.run_enclosure(rule(), dictionary))
                    continue

                rule.handle(dictionary)
                for error, message in rule.errors.items():
                    if error not in rule_errors:
                        rule_errors.update({error: message})
                    else:
                        messages = rule_errors[error]
                        messages += message
                        rule_errors.update({error: messages})
                rule.reset()
            return MessageBag(rule_errors)

        except Exception as e:
            e.errors = rule_errors
            raise e

        return MessageBag(rule_errors)

    def parse_string(self, rule):
        rule, parameters = rule.split(":")[0], rule.split(":")[1].split(",")
        return ValidationFactory().registry[rule](parameters)

    def parse_dict(self, rule, dictionary, rule_errors):
        for value, rules in rule.items():
            for rule in rules.split("|"):
                rule, args = rule.split(":")[0], rule.split(":")[1:]
                rule = ValidationFactory().registry[rule](value, *args)

                rule.handle(dictionary)
                for error, message in rule.errors.items():
                    if error not in rule_errors:
                        rule_errors.update({error: message})
                    else:
                        messages = rule_errors[error]
                        messages += message
                        rule_errors.update({error: messages})

    def run_enclosure(self, enclosure, dictionary):
        rule_errors = {}
        for rule in enclosure.rules():
            rule.handle(dictionary)
            for error, message in rule.errors.items():
                if error not in rule_errors:
                    rule_errors.update({error: message})
                else:
                    messages = rule_errors[error]
                    messages += message
                    rule_errors.update({error: messages})
            rule.reset()
        return rule_errors

    def extend(self, key, obj=None):
        if isinstance(key, dict):
            self.__dict__.update(key)
            return self

        self.__dict__.update({key: obj})
        return self

    def register(self, *cls):
        for obj in cls:
            self.__dict__.update({obj.__name__: obj})
            ValidationFactory().register(obj)


class ValidationFactory:
    registry = {}

    def __init__(self):
        self.register(
            accepted,
            boolean,
            active_domain,
            after_today,
            before_today,
            confirmed,
            contains,
            date,
            does_not,
            different,
            distinct,
            equals,
            email,
            exists,
            exists_in_db,
            not_exists_in_db,
            file,
            greater_than,
            image,
            in_range,
            is_future,
            is_in,
            isnt,
            is_list,
            is_past,
            ip,
            json,
            length,
            less_than,
            matches,
            none,
            numeric,
            one_of,
            phone,
            postal_code,
            regex,
            required,
            required_if,
            required_with,
            string,
            strong,
            timezone,
            truthy,
            unique_in_db,
            uuid,
            video,
            when,
        )

    def register(self, *cls):
        for obj in cls:
            self.registry.update({obj.__name__: obj})
